﻿#region File and License Information
/*
<File>
	<Copyright>Copyright © 2009, Daniel Vaughan. All rights reserved.</Copyright>
	<License>
	This file is part of DanielVaughan's base library

    DanielVaughan's base library is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    DanielVaughan's base library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with DanielVaughan's base library.  If not, see http://www.gnu.org/licenses/.
	</License>
	<Owner Name="Daniel Vaughan" Email="dbvaughan@gmail.com"/>
	<CreationDate>2010-02-06 18:16:24Z</CreationDate>
</File>
*/
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace DanielVaughan.TaskModel
{
	/// <summary>
	/// Provides the ability to group tasks for sequential or parallel execution 
	/// with undo and redo capabilities.
	/// </summary>
	public class CompositeUndoableTask<T> : UndoableTaskBase<T>
	{
		readonly string descriptionForUser;
		readonly Dictionary<UndoableTaskBase<T>, T> taskDictionary;

		public CompositeUndoableTask(IDictionary<UndoableTaskBase<T>, T> tasks, string descriptionForUser)
		{
			ArgumentValidator.AssertNotNull(descriptionForUser, "descriptionForUser");
			ArgumentValidator.AssertNotNull(tasks, "tasks");
			this.descriptionForUser = descriptionForUser;
			taskDictionary = new Dictionary<UndoableTaskBase<T>, T>(tasks);
			Execute += OnExecute;
			Undo += OnUndo;

			/* Determine repeatable status. */
			bool repeatable = taskDictionary.Keys.Count > 0;
			foreach (IInternalTask key in tasks.Keys)
			{
				if (!key.Repeatable)
				{
					repeatable = false;
				}
			}
			Repeatable = repeatable;
		}
        
		void OnExecute(object sender, TaskEventArgs<T> e)
		{
			ExecuteInternal(taskDictionary);
		}

		protected internal virtual void ExecuteInternal(Dictionary<UndoableTaskBase<T>, T> taskDictionary)
		{
			if (Parallel)
			{
				ExecuteInParallel(taskDictionary);
			}
			else
			{
				ExecuteSequentially(taskDictionary);
			}
		}

		static void ExecuteSequentially(Dictionary<UndoableTaskBase<T>, T> taskDictionary)
		{
			var performedTasks = new List<UndoableTaskBase<T>>();
			foreach (KeyValuePair<UndoableTaskBase<T>, T> pair in taskDictionary)
			{
				var task = (IInternalTask)pair.Key;
				try
				{
					task.PerformTask(pair.Value);
					performedTasks.Add(pair.Key);
				}
				catch (Exception)
				{
					/* TODO: improve this to capture undone task errors. */
					SafelyUndoTasks(performedTasks.Cast<IUndoableTask>());
					throw;
				}
			}
		}

		static void SafelyUndoTasks(IEnumerable<IUndoableTask> undoableTasks)
		{
			try
			{
				foreach (var undoableTask in undoableTasks)
				{
					try
					{
						undoableTask.Undo();
					}
					catch (Exception ex)
					{
						Console.WriteLine(ex);
						/* Ignore for now. TODO: implement internal log. */
					}
				}
			}
			catch (Exception ex)
			{
				Console.WriteLine(ex);
			}
		}

		void OnUndo(object sender, TaskEventArgs<T> e)
		{
			UndoInternal(taskDictionary);
		}

		protected internal virtual void UndoInternal(Dictionary<UndoableTaskBase<T>, T> taskDictionary)
		{
			if (Parallel)
			{
				UndoInParallel(taskDictionary);
			}
			else
			{
				UndoSequentially(taskDictionary);
			}
		}

		static void UndoSequentially(Dictionary<UndoableTaskBase<T>, T> taskDictionary)
		{
			/* Undo sequentially. */
			foreach (KeyValuePair<UndoableTaskBase<T>, T> pair in taskDictionary)
			{
				var undoableTask = (IUndoableTask)pair.Key;
				undoableTask.Undo();
			}
		}

		public override string DescriptionForUser
		{
			get
			{
				return descriptionForUser;
			}
		}

		#region Parallel Execution
		public bool Parallel { get; set; }

		static void ExecuteInParallel(Dictionary<UndoableTaskBase<T>, T> taskDictionary)
		{
			/* When we move to .NET 4 we may use System.Threading.Parallel for the Desktop CLR. */
			var performedTasks = new List<UndoableTaskBase<T>>();
			object performedTasksLock = new object();
			var exceptions = new List<Exception>();
			object exceptionsLock = new object();
			var events = taskDictionary.ToDictionary(x => x, x => new AutoResetEvent(false));

			foreach (KeyValuePair<UndoableTaskBase<T>, T> pair in taskDictionary)
			{
				var autoResetEvent = events[pair];
				var task = (IInternalTask)pair.Key;
				var undoableTask = pair.Key;
				var arg = pair.Value;

				ThreadPool.QueueUserWorkItem(
					delegate
					{
						try
						{
							task.PerformTask(arg);
							lock (performedTasksLock)
							{
								performedTasks.Add(undoableTask);
							}
						}
						catch (Exception ex)
						{
							/* TODO: improve this to capture undone task errors. */
							lock (exceptionsLock)
							{
								exceptions.Add(ex);
							}
						}
						autoResetEvent.Set();
					});

			}

			foreach (var autoResetEvent in events.Values)
			{
				autoResetEvent.WaitOne();
			}

			if (exceptions.Count > 0)
			{
				SafelyUndoTasks(performedTasks.Cast<IUndoableTask>());
				throw new CompositeException("Unable to undo tasks", exceptions);
			}
		}

		static void UndoInParallel(Dictionary<UndoableTaskBase<T>, T> taskDictionary)
		{
			/* When we move to .NET 4 we may use System.Threading.Parallel for the Desktop CLR. */
			var performedTasks = new List<UndoableTaskBase<T>>();
			object performedTasksLock = new object();
			var exceptions = new List<Exception>();
			object exceptionsLock = new object();
			var events = taskDictionary.ToDictionary(x => x, x => new AutoResetEvent(false));

			foreach (KeyValuePair<UndoableTaskBase<T>, T> pair in taskDictionary)
			{
				var autoResetEvent = events[pair];
				var undoableTask = pair.Key;

				ThreadPool.QueueUserWorkItem(
					delegate
					{
						try
						{
							((IUndoableTask)undoableTask).Undo();
							lock (performedTasksLock)
							{
								performedTasks.Add(undoableTask);
							}
						}
						catch (Exception ex)
						{
							/* TODO: improve this to capture undone task errors. */
							lock (exceptionsLock)
							{
								exceptions.Add(ex);
							}
						}
						autoResetEvent.Set();
					});

			}

			foreach (var autoResetEvent in events.Values)
			{
				autoResetEvent.WaitOne();
			}

			if (exceptions.Count > 0)
			{
				SafelyUndoTasks(performedTasks.Cast<IUndoableTask>());
				throw new CompositeException("Unable to undo tasks", exceptions);
			}
		}
		#endregion
	}
}
